Server pairing: confirm in #setup before adding (Accept/Dismiss prompt) - #48
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iring Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… prompts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DiscordChannelMessenger.EnsureAsync returns null on any post failure (including a stale channel id from the locator's 30s TTL cache), but HandleDetectedAsync stored the pending pairing regardless. A null-MessageId pending permanently short-circuited every later in-game "Pair" press into the refresh branch, so no prompt ever existed and the endpoint was stuck until restart. Now a failed post logs a warning and drops the pairing so re-pairing in-game retries cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR changes the server-pairing flow so that newly detected servers are not immediately registered; instead, the bot posts an Accept/Dismiss prompt in the guild’s #setup channel and only proceeds with registration when accepted, aligning the behavior with other pairing coordinators.
Changes:
- Routes new server pairings to a new
ServerPairingCoordinatorthat posts an Accept/Dismiss prompt in#setupand replays registration only on Accept. - Introduces a guild-global
ISetupChannelLocator/SetupChannelLocatorto resolve the provisioned#setupchannel row (server id null) with TTL caching. - Adds prompt rendering + interaction handling (component module), plus localization keys and expanded test coverage across pairing/workspace/localization.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/RustPlusBot.Localization.Tests/StringsResourceParityTests.cs | Updates expected localization key count for added server prompt strings. |
| tests/RustPlusBot.Features.Workspace.Tests/WorkspaceRegistrationTests.cs | Asserts workspace DI now registers the new ISetupChannelLocator. |
| tests/RustPlusBot.Features.Workspace.Tests/Locating/SetupChannelLocatorTests.cs | Adds coverage for resolving/caching the global #setup channel row. |
| tests/RustPlusBot.Features.Pairing.Tests/ServerPairingPromptRendererTests.cs | Tests prompt/confirmation rendering and component-id tail parsing. |
| tests/RustPlusBot.Features.Pairing.Tests/ServerPairingCoordinatorTests.cs | Adds coordinator behavior tests including concurrency + failure/drop paths. |
| tests/RustPlusBot.Features.Pairing.Tests/PairingRegistrationTests.cs | Ensures pairing DI resolves the new coordinator and related services. |
| tests/RustPlusBot.Features.Pairing.Tests/PairingHandlerTests.cs | Updates server pairing tests for “new routes to coordinator / existing silent upsert”. |
| tests/RustPlusBot.Features.Pairing.Tests/Fakes/RecordingOwnerNotifier.cs | Extends notifier fake to record “#setup missing” notifications. |
| tests/RustPlusBot.Features.Pairing.Tests/DiscordOwnerNotifierTests.cs | Adds test for the new owner DM path when #setup is missing. |
| src/RustPlusBot.Localization/Strings.resx | Adds English strings for server pairing prompt + confirmation. |
| src/RustPlusBot.Localization/Strings.fr.resx | Adds French strings for server pairing prompt + confirmation. |
| src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs | Registers ISetupChannelLocator in workspace DI. |
| src/RustPlusBot.Features.Workspace/Locating/SetupChannelLocator.cs | Implements guild-global #setup channel locator with TTL cache and scoped store reads. |
| src/RustPlusBot.Features.Workspace/Locating/ISetupChannelLocator.cs | Adds public abstraction for resolving the guild-global #setup channel id. |
| src/RustPlusBot.Features.Pairing/RustPlusBot.Features.Pairing.csproj | Adds localization project reference for prompt rendering strings. |
| src/RustPlusBot.Features.Pairing/Rendering/ServerPairingPromptRenderer.cs | Renders the server “Add it?” prompt and post-accept confirmation embed. |
| src/RustPlusBot.Features.Pairing/Rendering/ServerPairingComponentIds.cs | Defines Accept/Dismiss custom-id prefixes and {ip}:{port} tail parsing. |
| src/RustPlusBot.Features.Pairing/Posting/ISetupChannelPoster.cs | Introduces abstraction for posting/editing prompts in #setup. |
| src/RustPlusBot.Features.Pairing/Posting/DiscordSetupChannelPoster.cs | Discord-backed poster shim using the shared channel messenger. |
| src/RustPlusBot.Features.Pairing/PairingServiceCollectionExtensions.cs | Wires new coordinator/prompt/poster into pairing DI. |
| src/RustPlusBot.Features.Pairing/Pairing/ServerPairingCoordinator.cs | Holds pending new-server pairings and performs registration on Accept. |
| src/RustPlusBot.Features.Pairing/Pairing/PairingHandler.cs | Splits server pairing handling: new → coordinator prompt; existing → silent upsert/backfill. |
| src/RustPlusBot.Features.Pairing/Pairing/IServerPairingCoordinator.cs | Defines coordinator interface + accept outcome enum. |
| src/RustPlusBot.Features.Pairing/Notifications/IOwnerNotifier.cs | Adds notifier method for “#setup missing” DM. |
| src/RustPlusBot.Features.Pairing/Notifications/DiscordOwnerNotifier.cs | Implements the “#setup missing” owner DM message. |
| src/RustPlusBot.Features.Pairing/Modules/ServerPairingComponentModule.cs | Handles Accept/Dismiss button interactions for the server prompt. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var key = (guildId, notification.Ip, notification.Port); | ||
| if (_pending.TryGetValue(key, out var existing)) | ||
| { | ||
| // Repeat in-game "Pair" press: keep the prompt, refresh the held pairing (latest token wins). | ||
| _pending[key] = new Pending(ownerUserId, notification, existing.MessageId); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Fixed in 5827525. Verified the concern reproduces: with the old refresh branch a re-pair only updated the token, so a moderated/deleted prompt left the pairing pending with no message to accept until a restart — and it contradicted the class's own "re-pair to re-prompt" doc.
The two detection branches are now unified: EnsureAsync is seeded with the known message id (null on first detection). Since DiscordChannelMessenger.EnsureAsync edits when the message exists (RenderGate skips identical re-renders, so no PATCH is spent) and reposts when the id is 404/deleted, a repeat "Pair" press self-heals a deleted prompt and tracks the new id, while never posting a duplicate — the detect-gate still serializes concurrent detections. First-post failure still drops pending so a re-pair retries (the 300db04 contract); a transient re-ensure failure with a prompt already shown keeps the live prompt/pending untouched.
Covered by a new test (Repeat_detection_reensures_prompt_so_a_deleted_message_self_heals) plus updated concurrency/refresh assertions. 977 passed + 1 skipped across all assemblies; format gate clean.
…leted message self-heals A re-detected pairing refreshed the held token but returned without re-ensuring the prompt. If the original #setup prompt was deleted, every later re-pair kept the pairing pending yet never recreated the prompt, blocking acceptance until a restart cleared pending state. Seed EnsureAsync with the known message id (null on first detection) so an edit self-heals a deleted prompt (repost) and a repeat press never posts a duplicate; the gate still serializes detections. First-post failure still drops pending (retry on re-pair); a transient re-ensure failure keeps the live prompt/pending untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
New-server FCM pairings no longer auto-run the full registration cycle. Instead, a "New server detected — Add it?" prompt (Accept/Dismiss buttons) is posted in the guild's #setup channel, mirroring the switch/alarm/storage-monitor pairing flow. Only Accept replays the registration path (persist server + credential, publish
ServerRegisteredEvent→ channel provisioning + Rust+ connect).ServerPairingCoordinator, exactSwitchPairingCoordinatormirror: no DB, no expiry, lost on restart → re-pair re-prompts) + prompt in #setup via newISetupChannelLocator(guild-global sibling ofCachingChannelLocator).PairingHandlerserver path, using the pairing owner's identity/token; prompt edited into a buttonless "Server added" embed. Dismiss → pending dropped, prompt deleted.server.prompt.*keys (en + fr), catalog count guard 268 → 274.Test plan
-maxcpucount:1), baseline was 947+1jb cleanupcode --profile=ReformatAndReordergate clean🤖 Generated with Claude Code